package com.pocket.insight.base.mql.relation;

import cn.hutool.db.dialect.impl.OracleDialect;
import com.google.common.base.Strings;
import com.pocket.insight.base.exception.BizException;
import com.pocket.insight.base.mql.Conversion;
import com.pocket.insight.base.mql.EvalAdapter;
import com.pocket.insight.base.mql.Relation;
import com.pocket.insight.base.mql.TypedVal;
import com.pocket.insight.base.sql.BaseSchemaProvider;
import com.pocket.insight.base.sql.Dialect;
import com.pocket.insight.base.sql.SchemaProvider;
import com.pocket.insight.base.sql.SqlEval;
import com.pocket.insight.base.sql.dialect.MongoDialect;
import com.pocket.insight.base.sql.dialect.MysqlDialect;
import com.pocket.insight.controller.dto.SqlParam;
import com.pocket.insight.entity.Dataset;
import com.pocket.insight.entity.ME;
import com.pocket.insight.entity.Type;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;

import java.util.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;

/**
 * 描述：
 *
 * @author shuize
 * @create 2024/12/5
 */
public class StatementRelation extends BaseRelation {
    public  List<ME>                   select   = new ArrayList<>();
    public  List<ME>                   where    = new ArrayList<>();
    public  List<ME>                   having   = new ArrayList<>();
    public  List<ME>                   orderBy  = new ArrayList<>();
    public  Relation                   from;
    private Map<String, Dataset.Field> schemaMap;
    public  Long                       offset;
    public  Long                       limit;
    public  SqlEval.EvalMode           evalMode = SqlEval.EvalMode.EVAL_MODE_DATASET;
    public  List<String>               noGroupUid;
    public  boolean                    isUnionSubRelation;                     // 是否为联合子关系查询
    public  List<Relation>             with     = new ArrayList();
    public  boolean                    ignoreCastError;



    public StatementRelation scan(Relation relation) {
        this.from = relation;
        return this;
    }

    public StatementRelation where(List<ME> filters) {
        if (CollectionUtils.isNotEmpty(filters)) {
            if (this.where == null) {
                this.where = new ArrayList();
            }
            this.where.addAll(filters);
        }
        return this;
    }

    public StatementRelation having(ME filter) {
        this.having.add(filter);
        return this;
    }

    public StatementRelation having(List<ME> filters) {
        if (CollectionUtils.isNotEmpty(filters)) {
            this.having.addAll(filters);
        }
        return this;
    }

    public StatementRelation sort(ME order) {
        this.orderBy.add(order);
        return this;
    }

    public StatementRelation sort(List<ME> orders) {
        if (CollectionUtils.isNotEmpty(orders)) {
            this.orderBy.addAll(orders);
        }
        return this;
    }

    public StatementRelation offset(Long offset) {
        this.offset = offset;
        return this;
    }

    public StatementRelation limit(Long limit) {
        this.limit = limit;
        return this;
    }

    public StatementRelation alias(String alias) {
        this.alias = alias;
        return this;
    }

    public StatementRelation with(Relation relation) {
        if (this.with.stream().noneMatch((r) -> {
            return StringUtils.equals(r.getAlias(), relation.getAlias());
        })) {
            this.with.add(relation);
        }
        return this;
    }

    public StatementRelation with(List<Relation> relation) {
        if (relation != null) {
            relation.forEach(this::with);
        }

        return this;
    }
    public StatementRelation(Dialect dialect) {
        this.evalMode = SqlEval.EvalMode.EVAL_MODE_DATASET;
        this.noGroupUid = new ArrayList();
        this.dialect = dialect;
    }

    public StatementRelation project(ME field) {
        this.select.add(field);
        return this;
    }

    public StatementRelation project(List<ME> fields) {
        if (CollectionUtils.isNotEmpty(fields)) {
            this.select.addAll(fields);
        }
        return this;
    }

    public boolean isEffectiveSql() {
        return !this.select.isEmpty() || !this.where.isEmpty() || !this.having.isEmpty() || !this.orderBy.isEmpty() || this.offset != null || this.limit != null;
    }

    @Override
    public TypedVal toQuery() {
        if (fetchRunTime().query != null) {
            return fetchRunTime().query;
        }
        if (this.from != null && this.from.getIsSourceVirtualDialect(getDialect())) {
            this.from.setDialect(getDialect());
        }

        StatementRelation relation = this;
        AtomicBoolean anyAggregate = new AtomicBoolean(false);

        Boolean hasField = CollectionUtils.isEmpty(this.select)
                && CollectionUtils.isNotEmpty(this.from.fetchRunTime().fieldsTouched)
                && CollectionUtils.emptyIfNull(this.fetchRunTime().relatedExprs).stream().noneMatch(item -> ME.isConstantStar(item));
        if (hasField) {
            boolean[] toAddFields = new boolean[]{false};
            SchemaProvider schemaProvider = this.from.getSchemaProvider();
            List<ME> selects = this.from.fetchRunTime().fieldsTouched.stream().map(item -> {
                ME f = ME.create(ME.Kind.field, item.getLeft(), null, item.getLeft()).setDataset(item.getRight());
                if (schemaProvider != null && !toAddFields[0]) {
                    Dataset.Field field = schemaProvider.getField(f);
                    if (field != null && field.getDbFieldName() != null) {
                        toAddFields[0] = true;
                    }
                }
                return f;
            }).collect(Collectors.toList());
            if (this.from.getAlias() != null && !this.from.getAlias().equals(this.getAlias()) || toAddFields[0]) {
                this.project(selects);
                this.fetchRunTime().clearSelectWhenReuse = true;
            }
        }
        this.fetchRunTime().query = relation.toSqlImpl(anyAggregate);
        return fetchRunTime().query;
    }

    public StatementRelation copyRelation() {
        StatementRelation newRelation = this.copyRelationWithoutWith();
        newRelation.with(this.with);
        return newRelation;
    }

    public StatementRelation copyRelationWithoutWith() {
        StatementRelation newRelation = new StatementRelation(this.dialect);
        newRelation.scan(this.from);
        newRelation.project(this.select);
        newRelation.evalMode = this.evalMode;
        newRelation.where(this.where);
        newRelation.having(this.having);
        newRelation.sort(this.orderBy);
        newRelation.offset(this.offset);
        newRelation.limit(this.limit);
        newRelation.alias(this.alias);
        newRelation.noGroupUid = this.noGroupUid;
        newRelation.isUnionSubRelation = this.isUnionSubRelation;
        return newRelation;
    }


    public TypedVal toSqlImpl(AtomicBoolean anyAggregate) {
        if (!this.isEffectiveSql()){
            if (StringUtils.isNotBlank(this.alias)) {
                this.from.setAlias(this.alias);
            }
            return this.from.toQuery();
        }

        this.offset = this.offset == null ?  0L : this.offset;
        this.limit = (this.limit != null && this.limit < 0L) ? null : this.limit;

        SqlEval eval = SqlEval.createEval(this.from);
        eval.tableQuery = this.getFromQuery(this.from, this.from == null ? false : !this.from.fetchRunTime().relatedExprs.isEmpty() && CollectionUtils.isEmpty(this.select), true, this.from != null && (this.from.fetchRunTime().accessCount.equals(this.fetchRunTime().accessCount) || this.from.getAlias() != null && this.from.getAlias().equals(this.getAlias())));
        eval.schemaProvider = this.from != null ? this.from.getSchemaProvider() : null;
        if (this.from == null) {
            eval.setDialect(this.getDialect());
        }

        Map<String, TypedVal> valMap = new HashMap();
        Map<String, TypedVal> refMap = new TreeMap();

        List<TypedVal> selectVal = this.selectToVal(eval, this.select, valMap, refMap, anyAggregate);
        this.schemaMap = getStatementRelationSchemaMap(refMap, this.getAlias(), this.getConversion());
        List<TypedVal> sortVal = eval.tableQuery == null ? null : this.sortToVal(eval, this.orderBy);
        Boolean allAggConstant = CollectionUtils.isEmpty(selectVal) ? false : selectVal.stream().allMatch((val) -> {
            return val.getScope() == TypedVal.EvalScope.constant && !"*".equals(val.getStr());
        }) && this.evalMode == SqlEval.EvalMode.EVAL_MODE_AGGREGATE;
        TypedVal whereVal = eval.tableQuery != null && !allAggConstant ? this.filterToVal(eval, this.where) : null;
        if (this.dialect instanceof MysqlDialect) {
            eval.setRefMap(refMap);
        } else if (this.dialect instanceof MongoDialect && eval.schemaProvider != null) {
            eval.schemaProvider.fetchSchemaMap().putAll(this.schemaMap);
        }

        TypedVal havingVal = eval.tableQuery != null && !allAggConstant ? this.filterToVal(eval, this.having) : null;
        return this.getConversion().statementQueryPack(new Conversion.QueryParts(selectVal, whereVal, havingVal, sortVal, eval.tableQuery, this.offset, this.limit, this.alias, valMap, allAggConstant), this);
    }

    private List<TypedVal> selectToVal(SqlEval eval, List<ME> select, Map<String, TypedVal> valMap, Map<String, TypedVal> refMap, AtomicBoolean anyAggregate) {
        if (select.isEmpty()) {
            return null;
        } else {
            List<ME> finalSelect = select;
            if (this.fetchRunTime().fieldsTouched != null && !this.fetchRunTime().fieldsTouched.isEmpty()) {
                finalSelect = (List)select.stream().filter((e) -> {
                    return e.uid == null || this.fetchRunTime().fieldsTouched.stream().anyMatch((f) -> {
                        return e.uid.equals(f.getLeft());
                    });
                }).collect(Collectors.toList());
            }

            List<Integer> orderedIndex = new ArrayList();
            List<ME> orderedSelect = this.getOrderedSelect(finalSelect, orderedIndex);
            return this.getOrderedVals(eval, anyAggregate, valMap, refMap, orderedSelect, orderedIndex);
        }
    }


    // 处理字段排序
    protected List<ME> getOrderedSelect(List<ME> select, List<Integer> orderedIndex) {
        return select;
    }

    protected List<TypedVal> getOrderedVals(SqlEval eval, AtomicBoolean anyAggregate, Map<String, TypedVal> valMap, Map<String, TypedVal> refMap, List<ME> orderedSelect, List<Integer> orderedIndex) {
        List<TypedVal> orderedVals = orderedSelect.stream().map((arg) -> {
            if (arg.kind == ME.Kind.constant && arg.op instanceof ME.Star) {
                TypedVal val;
                if (this.dialect instanceof OracleDialect && StringUtils.isNotBlank(this.from.getAlias())) {
                    Dialect var12 = this.dialect;
                    val = new TypedVal(var12.quoteTableName(this.from.getAlias()) + ".*");
                } else if (arg.dataset != null && arg.dataset instanceof String && StringUtils.isNotBlank((String)arg.dataset) && StringUtils.isNotBlank(this.from.getAlias())) {
                    String var10002 = this.dialect.quoteTableName((String)arg.dataset);
                    val = new TypedVal(var10002 + ".*");
                } else {
                    val = new TypedVal("*");
                }

                val.setScope(TypedVal.EvalScope.row);
                return val;
            } else {
                String backupUid = null;
                if (arg.kind == ME.Kind.dataset) {
                    backupUid = arg.uid;
                    arg.uid = null;
                }

                eval.prepare(this.evalMode);
                TypedVal valx = findConstantVal(arg, this.from);
                if (valx == null) {
                    valx = eval.evaluate(arg);
                } else if (StringUtils.isBlank(arg.uid) && arg.op instanceof String) {
                    arg.uid = (String)arg.op;
                }
                if (backupUid != null) {
                    arg.uid = backupUid;
                }
                if (valx.getType() == Type.TypeName.bool && !this.dialect.isBoolAvailable()) {
                    throw new BizException("ERROR_HQL_BOOL_TYPE_NOT_SUPPORTED: " + this.dialect.getDialectName());
                } else {
                    valx.setScope(eval.getScope());
                    if (valx.getScope() != null && valx.getScope().compareTo(TypedVal.EvalScope.aggregate) >= 0) {
                        anyAggregate.set(true);
                    }

                    valx.setAlias(arg.uid);
                    if (valx.getScope() != TypedVal.EvalScope.constant && valx.getScope() != TypedVal.EvalScope.distinct && this.offset != 0L && this.dialect.isOffsetLimitRequiresOrderBy() && this.orderBy.isEmpty() && !this.select.isEmpty()) {
                        this.sort(arg);
                    }

                    if (valx.getScope() != TypedVal.EvalScope.constant && valMap.keySet().contains(valx.getStr()) && this.dialect.getClass() == MysqlDialect.class) {
                        valx.setStr(MysqlDialect.differentiateByIndex(valx.getStr(), valMap.size()));
                    }

                    this.dialect.quoteStringConstant(arg, valx);
                    if (StringUtils.isNotBlank(arg.uid)) {
                        eval.putRef(arg.uid, valx);
                        refMap.put(arg.uid, (new TypedVal(this.dialect.quoteAliasName(arg.uid), valx.getScope(), valx.getAlias())).setType(valx.getType()));
                        if (valx.getStr() != null) {
                            valMap.put(valx.getStr(), valx);
                        }

                        if (valx.getScope() == TypedVal.EvalScope.constant && arg.kind != ME.Kind.dataset) {
                            this.fetchRunTime().constantSelectUidValMap.put(arg.uid, valx);
                        }
                    }

                    return valx;
                }
            }
        }).collect(Collectors.toList());
        List<TypedVal> vals = new ArrayList(orderedVals.size());
        if (!orderedIndex.isEmpty()) {
            for(int i = 0; i < orderedIndex.size(); ++i) {
                for(int j = 0; j < orderedIndex.size(); ++j) {
                    if (i == (Integer)orderedIndex.get(j)) {
                        ((List)vals).add((TypedVal)orderedVals.get(j));
                    }
                }
            }
        } else {
            vals = orderedVals;
        }

        return vals;
    }


    protected static TypedVal findConstantVal(ME arg, Relation from) {
        if (from != null && arg.kind == ME.Kind.field && arg.op != null && arg.op instanceof String && arg.dataset != null) {
            if (from instanceof StatementRelation && from.getAlias() != null && from.getAlias().equals(arg.dataset)) {
                return from.fetchRunTime().constantSelectUidValMap.get(arg.op);
            } else {
                if (from instanceof JoinRelation) {
                    JoinRelation joinRelation = (JoinRelation)from;
                    TypedVal val = null;
                    if (joinRelation.joinType == Dataset.JOIN_TYPE.LEFT_JOIN) {
                        val = findConstantVal(arg, ((JoinRelation)from).left);
                    } else if (joinRelation.joinType == Dataset.JOIN_TYPE.RIGHT_JOIN) {
                        val = findConstantVal(arg, ((JoinRelation)from).right);
                    } else {
                        val = findConstantVal(arg, ((JoinRelation)from).left);
                        if (val != null) {
                            return val;
                        }

                        val = findConstantVal(arg, ((JoinRelation)from).right);
                    }

                    if (val != null) {
                        return val;
                    }
                }
                return null;
            }
        } else {
            return null;
        }
    }


    private static Map<String, Dataset.Field> getStatementRelationSchemaMap(Map<String, TypedVal> selectsValMap, String relationAlias, Conversion conversion) {
        Map<String, Dataset.Field> schemaMap = new HashMap<>();
        if (selectsValMap == null || selectsValMap.isEmpty()) {
            return schemaMap;
        }
        selectsValMap.values().forEach(val -> {
            if (val.getAlias() != null) {
                Dataset.Field field = new Dataset.Field();
                field.setFieldName(val.getAlias());
                if (val.getType() != null) {
                    field.setType(val.getType());
                    field.getGeneratedOptions().setOriginType(val.getType());
                }
                conversion.updateStatementRelationFieldInfo(field, relationAlias);
                schemaMap.put(val.getAlias(), field);
            }
        });
        return schemaMap;
    }


    private List<TypedVal> sortToVal(SqlEval eval, List<ME> exprs) {
        return (exprs == null ? new ArrayList() : exprs.stream().map((expr) -> {
            TypedVal val;
            try {
                eval.prepare(this.evalMode);
                val = eval.evaluate(eval.getRefUids().contains(expr.uid) && expr.kind != ME.Kind.field ? expr : EvalAdapter.replaceFieldToRef(expr, eval.getRefUids()));
            } catch (Exception e) {
                throw new BizException("ERROR_MQL_CAST_FAIL");
            }

            val.setScope(eval.getScope());
            return val;
        }).collect(Collectors.toList()));
    }

    private TypedVal filterToVal(SqlEval eval, List<ME> exprs) {
        return exprs.isEmpty() ? null : eval.evaluate(ME.create(ME.Kind.function, "AND", (List)exprs.stream().map((e) -> {
            return e;
        }).collect(Collectors.toList())));
    }


    @Override
    public SchemaProvider getSchemaProvider() {
        if (this.select.isEmpty()) {
            return this.from.getSchemaProvider();
        } else {
            BaseSchemaProvider schemaProvider = BaseSchemaProvider.create();
            if (this.schemaMap != null) {
                schemaProvider.setSchemaMap(this.schemaMap);
                return schemaProvider;
            } else {
                Map<String, Dataset.Field> tmpMap = new HashMap();
                boolean anyConstantStar = false;
                Iterator var4 = CollectionUtils.emptyIfNull(this.select).iterator();

                while(true) {
                    ME expr;
                    do {
                        do {
                            label56:
                            do {
                                Dataset.Field field;
                                for(; var4.hasNext(); tmpMap.put(field.getFieldName(), field)) {
                                    expr = (ME)var4.next();
                                    if (expr.uid == null && (expr.kind != ME.Kind.field || !(expr.op instanceof String))) {
                                        continue label56;
                                    }
                                    field = new Dataset.Field();
                                    field.setFieldName(expr.uid == null ? (String)expr.op : expr.uid);
                                    if (expr.type != null) {
                                        field.setType(expr.type);
                                        field.getGeneratedOptions().setOriginType(expr.type);
                                    }
                                }
                                if (anyConstantStar && this.from != null && CollectionUtils.isNotEmpty(this.from.getSchemaProvider().getFields())) {
                                    this.from.getSchemaProvider().getFields().forEach((f) -> {
                                        tmpMap.put(f.getFieldName(), f);
                                    });
                                    schemaProvider.setSchemaMap(tmpMap);
                                } else if (!anyConstantStar) {
                                    schemaProvider.setSchemaMap(tmpMap);
                                    return schemaProvider;
                                }

                                return schemaProvider;
                            } while(anyConstantStar);
                        } while(expr.kind != ME.Kind.constant);
                    } while(!"*".equals(expr.op) && !(expr.op instanceof ME.Star));

                    anyConstantStar = true;
                }
            }
        }
    }

    @Override
    public void clearRunTime() {
        if (this.from != null) {
            this.from.clearRunTime();
        }
        if (fetchRunTime() != null && BooleanUtils.isTrue((fetchRunTime()).clearSelectWhenReuse)) {
            this.select.clear();
        }
        if (this.schemaMap != null) {
            this.schemaMap.clear();
        }
        super.clearRunTime();
    }

    public StatementRelation sqlParam(SqlParam sqlParam) {
        offset(sqlParam.offset);
        limit(sqlParam.limit);
        if (!Strings.isNullOrEmpty(sqlParam.sortField)) {
            ME sort = ME.create(ME.Kind.field, sqlParam.sortField);
            sort.direction = sqlParam.sortType;
            sort(sort);
        }
        return this;
    }

}
